package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

/*
/opt/MegaRAID/MegaCli/MegaCli64 -LDInfo -Lall -aALL：
RAID Level : Primary-1, Secondary-0, RAID Level Qualifier-0" #代表Raid 1
RAID Level : Primary-0, Secondary-0, RAID Level Qualifier-0" #代表Raid 0
RAID Level : Primary-5, Secondary-0, RAID Level Qualifier-3" #代表Raid 5
RAID Level : Primary-1, Secondary-3, RAID Level Qualifier-0" #代表Raid10
从拔盘，到插盘的过程磁盘状态的变化。
Device : Normal|Damage|Rebuild|Normal
Virtual Drive : Optimal|Degraded|Degraded|Optimal
Physical Drive : Online|Failed –> Unconfigured|Rebuild|Online
*/
var (
	wg sync.WaitGroup
	pdstatus sync.Map
	ldstatus sync.Map
	sleepTs int = 5
	addr = flag.String("listen-address", ":9099", "The address to listen on for HTTP requests")
	////使用GaugeVec类型可以为监控指标设置标签，这里为监控指标增加一个标签"job_type"
	megafiles = []string{"/opt/MegaRAID/MegaCli/MegaCli64","MegaCli64"}
	pdLabelString = []string{"deviceid", "slotnumber","status","sas_address","pd_type"}
	raidFirmwareState = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "raid_firmware_state_up",
		Help: "固件状态 正常为0",
	}, pdLabelString)
	raidMediaErrorCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "raid_media_error_count",
		Help: "表示磁盘可能错误,可能是磁盘有坏道,这个值不为0值得注意,数值越大,危险系数越高",
	}, pdLabelString)
	raidOtherErrorCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "raid_other_error_count",
		Help: "表示磁盘可能存在松动，可能需要重新再插入,正常为0",
	}, pdLabelString)
	raidPredictiveFailureCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "raid_predictive_failure_count",
		Help: "磁盘已经故障,正常为0",
	}, pdLabelString)
	ldLabelString = []string{"TargetId","Name","RaidLevel","Size","State"}
	raidNumberOfDrives = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "raid_number_of_drives",
		Help: "逻辑磁盘的数量",
	},ldLabelString)
)

type metricsMetaData struct {
    UpdatedAt int64
	Labels prometheus.Labels
}


type Pdlistinfo   struct {
	SlotNumber string
	DeviceId string
	MediaErrorCount int 
	OtherErrorCount int 
	PredictiveFailureCount int 
	FirmwareState string
	SASAddress string
	PdType string
}

type Ldlistinfo   struct {
	TargetId string
	Name string
	RaidLevel string
	Size string 
	State string 
	NumberOfDrives int 
}

func timeCost(start time.Time) {
	tc := time.Since(start)
	fmt.Printf("time cost: %v\n", tc)
}


func GetCommandLinuxCon(commandLinux string) ([]byte,int) {
	//需要执行命令： commandLinux
	cmd := exec.Command("/bin/bash", "-c", commandLinux)
	// 获取输入
	output,_ := cmd.StdoutPipe()
	defer output.Close()
	// 执行命令
	if err := cmd.Start(); err != nil {
		fmt.Println("command execution failed. Please check whether the command input is correct", err.Error())
		return nil,11
	}
	// fmt.Println(cmd.Args) //查看当前执行命令
	// cmdPid := cmd.Process.Pid //查看命令pi

	bytes, err := ioutil.ReadAll(output) // 读取输出
	if err != nil {
		fmt.Println("Abnormal printing, please check")
		return nil,11
	}

	var res int = 0
	if err := cmd.Wait(); err != nil {
		if ex, ok := err.(*exec.ExitError); ok {
			fmt.Printf("command exec error '%s' %v",cmd,err.Error())
			res = ex.Sys().(syscall.WaitStatus).ExitStatus() //获取命令执行返回状态，相当于shell: echo $?
			return nil,res
		}
	}
	return bytes,res
}


func init() {
	prometheus.MustRegister(raidFirmwareState,raidMediaErrorCount,raidOtherErrorCount,raidPredictiveFailureCount,raidNumberOfDrives)

	var execeble int
	for _,file := range megafiles {
		if _,err := exec.LookPath(file);err != nil{
			execeble++
		}
	}
	if execeble == len(megafiles) {
		panic("can not find any 'MegaCli64' executable\nplease run under command to install Megecli:\n\t wget http://mirror.cogentco.com/pub/misc/MegaCli-8.07.14-1.noarch.rpm && rpm -ivh MegaCli-8.07.14-1.noarch.rpm")
		// cmd := "wget http://mirror.cogentco.com/pub/misc/MegaCli-8.07.14-1.noarch.rpm && rpm -ivh MegaCli-8.07.14-1.noarch.rpm"
		// fmt.Printf("集群缺少Megacli包,开始安装\n\t %s",cmd)
		// res,_ := GetCommandLinuxCon(cmd)
		// for line := range strings.Split(string(res), "\n") {fmt.Println(line)}
	}
}

func main() {
	flag.Parse()
	go func(){
		for{LdPdhander();time.Sleep(time.Second*time.Duration(sleepTs))}
	}()
	wg.Add(1)
	
	 // 创建退出 ctx
	 stopCtx, _ := context.WithCancel(context.Background())
	 go cleaner(&stopCtx,&wg) 
	 wg.Add(1)  

	// 创建并启动 http server
	http.Handle("/metrics", promhttp.Handler())
	srv := http.Server{Addr:*addr}
	go func(srv *http.Server, g *sync.WaitGroup) {
        defer g.Done()
        _ = srv.ListenAndServe()
    }(&srv, &wg)
    wg.Add(1)
	
	fmt.Println("服务成功启动！")
	wg.Wait()
	// 退出
    // time.Sleep(time.Second * 10)
    // stopCancel() //通知
    _ = srv.Shutdown(context.Background())
    wg.Wait()
}

func cleaner(ctx *context.Context, g *sync.WaitGroup) { //清理器
	defer g.Done()
	for {
		select {
		case <- (*ctx).Done(): //等待通知
			return
		default:
			now := time.Now().Unix()
			pdstatus.Range(func(key, value any) bool {
				if now - value.(metricsMetaData).UpdatedAt > int64(sleepTs)+6 {
					fmt.Printf("clean key %v",key)
					raidFirmwareState.Delete(value.(metricsMetaData).Labels)
					raidPredictiveFailureCount.Delete(value.(metricsMetaData).Labels)
					raidMediaErrorCount.Delete(value.(metricsMetaData).Labels)
					raidOtherErrorCount.Delete(value.(metricsMetaData).Labels)
					pdstatus.Delete(key)
				}
				return true
			})
			ldstatus.Range(func(key, value any) bool {
				if now - value.(metricsMetaData).UpdatedAt > int64(sleepTs)+6 {
					fmt.Printf("clean key %v",key)
					raidNumberOfDrives.Delete(value.(metricsMetaData).Labels)
					ldstatus.Delete(key)
				}
				return true
			})
		}
		time.Sleep(time.Second*2)
	}
}

func Inter(str string) int {
	str = strings.TrimSpace(str)
	i,err := strconv.Atoi(str)
	if err != nil {
		fmt.Printf("Inter error : %v",err)
	}
	return i
}

func Pdlist() ([]Pdlistinfo,error){
	defer func() {
		if errMsg := recover(); errMsg != nil {
			fmt.Println(errMsg)
		}
	}() 
	resp,_ := GetCommandLinuxCon("/opt/MegaRAID/MegaCli/MegaCli64 -pdlist -aALL")  //磁盘信息
	lines := strings.Split(string(resp), "\n\n")
	var nu int
	var devicelist []Pdlistinfo
	devicelist = make([]Pdlistinfo, 0)
	Hander: 
		nu++
		iline := strings.Split(string(lines[nu]),"\n")
		if strings.LastIndex(lines[nu],"Enclosure") > -1 {
			var pd Pdlistinfo
			for _,v := range iline{
				switch{
				case strings.LastIndex(v,"Slot Number") > -1:
					pd.SlotNumber = strings.TrimSpace(strings.Split(v, ":")[1])
				case strings.LastIndex(v,"Device Id") > -1:
					pd.DeviceId = strings.TrimSpace(strings.Split(v, ":")[1])
					// nn := readfile("/tmp/a")  //调试清理器
					// pd.DeviceId = nn 
				case strings.LastIndex(v,"Media Error Count") > -1:
					pd.MediaErrorCount = Inter(strings.Split(v, ":")[1])
				case strings.LastIndex(v,"Other Error Count") > -1:
					pd.OtherErrorCount = Inter(strings.Split(v, ":")[1])
				case strings.LastIndex(v,"Predictive Failure Count") > -1:
					pd.PredictiveFailureCount = Inter(strings.Split(v, ":")[1])
				case strings.LastIndex(v,"PD Type") > -1:
					pd.PdType = strings.TrimSpace(strings.Split(v, ":")[1])
				}
			}
			nu++
			eline := strings.Split(string(lines[nu]),"\n")
			for _,v := range eline{
				if strings.LastIndex(v,"Firmware state") > -1 {pd.FirmwareState = strings.TrimSpace(strings.Split(v, ":")[1])} else if strings.LastIndex(v,"SAS Address(0)") > -1{pd.SASAddress = strings.TrimSpace(strings.Split(v,":")[1])
				}
			
			}
			devicelist = append(devicelist,pd)
			}
			if nu < len(lines)-1 {goto Hander}
	return devicelist,nil
}
	

func Ldlist() []Ldlistinfo{
	defer func() {
		if errMsg := recover(); errMsg != nil {
			fmt.Println(errMsg)
		}
	}() 
	resp,_ := GetCommandLinuxCon("/opt/MegaRAID/MegaCli/MegaCli64 -LDInfo -Lall -aALL")  //磁盘信息
	lines := strings.Split(string(resp), "\n\n")
	var driverlist []Ldlistinfo
	driverlist = make([]Ldlistinfo, 0)
	for _,l := range lines {
		if strings.LastIndex(l,"Virtual Drive") > -1 {
			var ld Ldlistinfo
			for _,v := range strings.Split(l,"\n"){
				if strings.LastIndex(v,"Target Id") >-1 {
					reg :=regexp.MustCompile(`\(([^)]+)\)`)  //过滤出括号内容
					m := reg.FindAllStringSubmatch(v,-1)
					ld.TargetId = strings.TrimSpace(strings.Split(m[0][1], ":")[1])
				} else if strings.LastIndex(v,"Name") >-1 {
					ld.Name = strings.TrimSpace(strings.Split(v,":")[1])
				} else if strings.LastIndex(v,"Size") == 0 {
					ld.Size = strings.TrimSpace(strings.Split(v,":")[1])
				} else if strings.LastIndex(v,"State") >-1 {
					ld.State = strings.TrimSpace(strings.Split(v,":")[1])
				} else if strings.LastIndex(v,"Number Of Drives") >-1 {
					ld.NumberOfDrives = Inter(strings.Split(v,":")[1])
				} else if strings.LastIndex(v,"RAID Level") >-1 {
					ld.RaidLevel = strings.TrimSpace(strings.Split(v,":")[1])
				} 
		}
		driverlist = append(driverlist, ld)
	}
}
	return driverlist
}

func readfile(name string) string{
	file, err := os.Open(name)
	if err != nil {
		fmt.Println("open file err ", err)
	}
	var s string
	// defer 及时关闭文件
	defer file.Close()
	// 创建一个*Reader ，是带缓冲区的 默认缓冲区是4096个字节
	reader := bufio.NewReader(file)
	// 循环读取文件类似于python 的while
	for {
		str, err := reader.ReadString('\n') //读到第一个'\n'符合结束
		if err == io.EOF {  
			break              //io.EOF 表示文件的末尾
		}
		s =str
	}
	return s
}

func LdPdhander() {
	// defer timeCost(time.Now())
	pdl,_ := Pdlist()
	for _,pd := range pdl{
		plb := prometheus.Labels{"deviceid": pd.DeviceId,"slotnumber":pd.SlotNumber,"status":pd.FirmwareState,"sas_address":pd.SASAddress,"pd_type":pd.PdType}
		raidMediaErrorCount.With(plb).Set(float64(pd.MediaErrorCount))
		raidOtherErrorCount.With(plb).Set(float64(pd.OtherErrorCount))
		raidPredictiveFailureCount.With(plb).Set(float64(pd.PredictiveFailureCount))
		var fwstate int 
		if strings.LastIndex(pd.FirmwareState,"Online") == -1 {fwstate = 1}
		raidFirmwareState.With(plb).Set(float64(fwstate))
		pdstatus.Store(pd,metricsMetaData{UpdatedAt: time.Now().Unix(),Labels: plb})  //记录标签更新时间
	}
	ldl := Ldlist()
	for _,ld := range ldl{
		llb := prometheus.Labels{"TargetId": ld.TargetId,"Name":ld.Name,"Size":ld.Size,"RaidLevel":ld.RaidLevel,"State":ld.State}
		raidNumberOfDrives.With(llb).Set(float64(ld.NumberOfDrives))
		ldstatus.Store(ld,metricsMetaData{UpdatedAt: time.Now().Unix(),Labels: llb})  //记录标签更新时间
	}
	
}
